module("gray_utils", package.seeall)

local bit = require("bit")
local ox = require("posix_c")

local MAX_FD = ox.getrlimit("NOFILE")
if not MAX_FD then MAX_FD = 256 end 

BUFSIZE = 2^13

ecode_success = 0
ecode_warn = 1
ecode_again = 2
ecode_fatal_error = 1000
ecode_env_exception = 1100
ecode_retry_after_modify = 1200
ecode_retry_later = 1300

-- error messages and error code
local emsg_fatal_error = "Fatal Error!"
local emsg_env_exception = "Exception of Nginx Environment!"
local emsg_retry_after_modify = "Please retry after modify!"
local emsg_retry_later = "Please retry later!"

local ecode_to_emsg = {}
ecode_to_emsg[ecode_fatal_error] = emsg_fatal_error
ecode_to_emsg[ecode_env_exception] = emsg_env_exception
ecode_to_emsg[ecode_retry_after_modify] = emsg_retry_after_modify
ecode_to_emsg[ecode_retry_later] = emsg_retry_later


function basename(filename)
   return string.match(filename, "/([^/]+/?)$") 
end


function clear(path)
    local f, msg = io.open(path, "w")
    if not f then return nil, msg end
    f:write("")
    f:close()
    return true
end

function is_file(path)
    local fstat, m  = ox.stat(path)
    if not fstat then return nil, m end
    if fstat.type == 'regular' then
        return true
    else
        return false
    end
end


function is_file_exist(filename)
    return ox.access(filename)
end


function readdir(path)
    if not path then return nil, "empty path" end
    return ox.dir(path)
end


function mkdir(path)
    local rc, msg = ox.mkdir(path)
    if rc then
        ox.chmod(path, "755")
    end
    return rc, msg
end


function copy(source, dest)
    local s, msg = io.open(source, "r")
    if not s then return nil, msg end
    local d, msg = io.open(dest, "w")
    if not d then s:close(); return nil, msg end
    d:write(s:read("*all"))
    s:close()
    d:close()
    ox.chmod(dest, "644")
    return true
end


function remove_dir(path)
    local entries, m = readdir(path)
    if entries then
        for _, filename in ipairs(entries) do
            if filename ~= '..' and filename ~= '.' then
                local fullname = path..filename
                if is_file(fullname) then
                    local rt, msg = os.remove(fullname)
                    if not rt then return nil, msg end
                else
                    return nil, "unknown directory: "..fullname 
                end
            end
        end
    end
    return os.remove(path)
end


function copy_dir(source, dest)
    local rt, msg, target_path
    if is_file_exist(dest) then
        target_path = dest .. basename(source)
    else
        target_path = dest
    end
    rt, msg = mkdir(target_path)
    if not rt then return nil, msg end
    local entries, m = readdir(source)
    if entries then
        for _, filename in ipairs(entries) do
            if filename ~= '..' and filename ~= '.' then
                local source_file = source..filename
                -- copy regular file only, skip directory
                if is_file(source_file) then
                    local dest_file = target_path..filename
                    rt, msg = copy(source_file, dest_file)
                    if not rt then return nil, msg end
                end
            end
        end
    end
    return true
end


function is_dir(path)
    local fstat, m  = ox.stat(path)
    if not fstat then return nil, m end
    if fstat.type == 'directory' then
        return true
    else
        return false
    end
end


function set_non_blocking(fd)
    ox.fcntl(fd, ox.F_SETFL, bit.bor(ox.fcntl(fd, ox.F_GETFL), ox.O_NONBLOCK))
end


-- we just need to read from pipe
function popen(command, stdout, stderr)
    local stdout_r, stdout_w, stderr_r, stderr_w
    if stdout then
        stdout_r, stdout_w = ox.pipe()
        if not stdout_r then return nil, stdout_w end
    end
    if stderr then
        stderr_r, stderr_w = ox.pipe()
        if not stderr_r then 
            if stdout then ox.close(stdout_r); ox.close(stdout_w) end
            return nil, stderr_w
        end
    end
    local pid, em = ox.fork()
    if not pid then
        return nil, em
    elseif pid == 0 then
        if stdout and stdout_w ~= 1 then ox.dup2(stdout_w, 1) end
        if stderr and stderr_w ~= 2 then ox.dup2(stderr_w, 2) end

        for fd = 3, MAX_FD do
            ox.close(fd)
        end

        ox.exec("/bin/sh", "-c", command)

        ox.exit(-127)
    end
    if stdout_w then ox.close(stdout_w) end
    if stderr_w then ox.close(stderr_w) end
    if stdout and stderr then 
        return stdout_r, stderr_r, pid
    elseif stdout then
        return stdout_r, pid
    elseif stderr then
        return stderr_r, pid
    else
        return pid
    end
end


function test_permission(pathname, mode)
    return ox.access(pathname, mode or "f")
end


function print_and_log(err_code, extr_str, no_escape)
    local status, out_json
    if err_code == "RAW" then
        out_json = extr_str
    else
        local err_str = ecode_to_emsg[err_code] or "unknown error"
        if type(extr_str) == 'string' then
            if not no_escape then
                extr_str = string.gsub(extr_str, "\"", "\\\"")
                extr_str = string.gsub(extr_str, "\n", "\\n")
            end
            if err_code == ecode_success then
                out_json = string.format("{\"status\":\"SUCCESS\",\"result\":[%s]}",
                                         extr_str)
            elseif err_code == ecode_warn then
                out_json = string.format("{\"status\":\"SUCCESS\",\"emsg\":\"%s\"}",
                                         extr_str)
            else
                out_json = string.format(
                    "{\"status\":\"ERROR\",\"ecode\":\"%d\",\"emsg\":\"%s\\n%s\"}",
                    err_code, err_str, extr_str) 
            end
        elseif type(extr_str) == 'table' and #extr_str > 0 then
            if err_code == ecode_success then
                out_json = "{\"status\":\"SUCCESS\",\"result\":["
            elseif err_code == ecode_warn then
                out_json = "{\"status\":\"SUCCESS\",\"emsg\":\""
            else
                out_json = string.format(
                    "{\"status\":\"ERROR\",\"ecode\":\"%d\",\"emsg\":\"%s\\n",
                    err_code, err_str) 
            end
        else
            if err_code == ecode_success then
                out_json = "{\"status\":\"SUCCESS\"}"
            elseif err_code == ecode_warn then
                out_json = "{\"status\":\"SUCCESS\"}"
            else
                out_json = string.format("{\"status\":\"ERROR\",\"ecode\":\"%d\"}",
                                         err_code) 
            end
        end
    end
    ngx.log(ngx.WARN, out_json)
    ngx.print(out_json)
    if type(extr_str) == 'table' then
        for i, v in ipairs(extr_str) do
            if not no_escape then
                v = string.gsub(v, "\"", "\\\"")
                v = string.gsub(v, "\n", "\\n")
            end
            if err_code == ecode_success then
                if i ~= #extr_str then
                    ngx.log(ngx.WARN, string.format("%s,", v))
                    ngx.print(string.format("%s,", v))
                else
                    ngx.log(ngx.WARN, string.format("%s]}", v))
                    ngx.print(string.format("%s]}", v))
                end
            else
                if i ~= #extr_str then
                    ngx.log(ngx.WARN, string.format("%s\\n", v))
                    ngx.print(string.format("%s\\n", v))
                else
                    ngx.log(ngx.WARN, string.format("%s\"}", v))
                    ngx.print(string.format("%s\"}", v))
                end
            end
        end
    end
end


function icheck(value, vtype, arg)
    local t = type(value)
    local vt = vtype
    if vtype == 'array' then vt = 'table' end

    if t ~= vt then return nil end

    if vt == 'string' then
        if value == "" then return nil  end
        if not arg and string.match(value, ";") then
            return nil
        else
            return value
        end
    elseif vt == 'table' then
        for k, v in pairs(value) do
            if vtype == 'array' then
                if not icheck(k, 'number') then return nil end
            else
                if not icheck(k, 'string', arg) then return nil end
            end
            if not icheck(v, 'string', arg) then return nil end
        end
        return value
    elseif vt == 'number' then
        return value
    else
        return nil
    end
end

getmetatable(gray_utils).__newindex = function (table, key, val)
    error('Attempt to write to undeclared variable "' .. key .. '": '
          .. debug.traceback())
end

